﻿"""

模块名称：conversion_rate_2_3_detect.py

范围编号：0-99

"""



# 导入必要的库

import pandas as pd

import json

from datetime import datetime

from sklearn import metrics

import matplotlib.pyplot as plt



# 定义一个全局异常映射字典

_anomaly_map = {}



# 生成当前时间的异常键值

def _gen_curr_anomaly_key(gid, os, time):

    return f"{gid}_{os}_{time}"



# 生成前一小时的时间异常键值

# @return: 前一个小时的键值字符串

def _gen_pre_hour_anomaly_key(gid, os, time):

    # 减去一个小时的时间差

    pre_hour_time = time - pd.to_timedelta(1, unit='h')

    return f"{gid}_{os}_{pre_hour_time}"



# 过滤掉已标记为异常的数据特征

# @return: 过滤后的特征列表

def filter_anomaly(gid, os, features):

    new_features = []

    for feature in features:

        # 生成当前特征的时间键值

        k = _gen_curr_anomaly_key(gid, os, feature['time'])

        # 如果键值存在于异常映射且计数大于1，则跳过此特征

        if k in _anomaly_map and _anomaly_map[k] > 1:

            continue

        # 否则将特征加入新列表

        new_features.append(feature)

    return new_features



# 判断是否为异常情况并更新异常映射

# @return: 是否为异常的布尔值

def is_anomaly(gid, os, time):

    global _anomaly_map

    

    # 生成前一小时的异常键值

    pre_key = _gen_pre_hour_anomaly_key(gid, os, time)

    # 生成当前时间的异常键值

    curr_key = _gen_curr_anomaly_key(gid, os, time)

    

    # 如果前一小时的键值存在于异常映射中

    if pre_key in _anomaly_map:

        # 更新当前时间的异常计数并返回True表示是异常

        _anomaly_map[curr_key] = _anomaly_map[pre_key] + 1

        return True

    else:

        # 否则初始化当前时间的异常计数为1

        _anomaly_map[curr_key] = 1

    # 返回False表示不是异常

    return False





# 定义一个函数用于检测值是否超出三倍标准差范围

def threesigma(df, detect_value):

    """

    检查给定值是否超出数据集平均值加减三个标准差的范围。



    参数:

    df (DataFrame): 包含数值列的Pandas DataFrame。

    detect_value (float): 需要检测的值。



    返回:

    bool: 如果检测值超出范围返回True，否则返回False。

    """

    mean_val = df['value'].mean()  # 计算均值

    std_dev = df['value'].std()  # 计算标准差

    lower_bound = mean_val - 3*std_dev  # 下界

    upper_bound = mean_val + 3*std_dev  # 上界

    if detect_value < lower_bound:

        return True

    return False



# 主程序入口

def main():

    """

    主函数执行流程：



    读取转换率数据并进行预处理，

    合并游戏统计信息，

    应用异常检测算法，

    输出结果到CSV文件，

    并计算评估指标。

    """



    # 加载转换率数据

    df_data = pd.read_csv("anomaly_detection/data/transformed1_conversion_rate_2_3_2024-07-23-17_06_19.csv")

    df_data['time'] = pd.to_datetime(df_data['time'])  # 转换时间戳为日期时间格式

    df_data['dt'] = pd.to_datetime(df_data['time'].dt.date)  # 提取日期部分



    # 加载游戏统计数据

    df_statinfo = pd.read_csv("anomaly_detection/data/game_statinfo1.csv")

    df_statinfo['dt'] = pd.to_datetime(df_statinfo['dt'])  # 转换日期字符串为日期时间对象



    # 数据合并

    df_data = df_data.merge(df_statinfo, on=['dt', 'gid', 'os'], how='left')

    df_data = df_data[(df_data.conversion_2_rolling_3days >= 300)]  # 过滤条件





    # 初始化分类器性能指标列表

    true_positives = []  # 真正例

    false_positives = []  # 假正例

    true_negatives = []  # 真反例

    false_negatives = []  # 假反例

    actual_labels = []  # 实际标签

    predicted_labels = []  # 预测标签



    # 打印总行数

    print("total:", df_data.shape[0])



    # 处理每条记录

    current_row = 0

    detections_dataframe = None

    for row in df_data.itertuples():

        current_row += 1

        if current_row % 1000 == 0:

            current_time = datetime.now()

            formatted_time = current_time.strftime("%H:%M:%S")  # 格式化当前时间

            print(formatted_time, current_row)



        game_id = row.gid

        operating_system = row.os

        timestamp = row.time

        anomaly_label = row.label

        feature_list = json.loads(row.features)  # 解析特征JSON字符串



        # 判断当前时间是否处于0-7点之间

        if timestamp.hour <= 9 and timestamp.hour >= 0:

            continue



        if len(feature_list) < 6:  # 忽略特征数量不足的情况

            continue



        feature_list = filter_anomaly(game_id, operating_system, feature_list)

        features_dataframe = pd.DataFrame(feature_list)

        features_dataframe['time'] = pd.to_datetime(features_dataframe['time'])  # 设置时间索引

        features_dataframe.set_index('time', inplace=True)



        actual_labels.append(anomaly_label)  # 添加实际标签至列表



        # 创建字典存储当前行的信息

        row_info = {

            "time": timestamp,

            "gid": game_id,

            "os": operating_system,

            "value": row.value,

            "label": anomaly_label,

            "features": row.features,

            "type": ""

        }



        # 异常检测

        # 判断当前行数据是否超出三倍标准差范围，并且是否被标记为异常

        if threesigma(features_dataframe, row.value) \
            and is_anomaly(game_id, operating_system, timestamp):

            predicted_labels.append(1)  # 预测为异常

            if anomaly_label == 1:

                true_positives.append([timestamp, game_id, operating_system, row.value, anomaly_label])

                row_info["type"] = "tp"  # 真正例

            else:

                false_positives.append([timestamp, game_id, operating_system, row.value, anomaly_label])

                row_info["type"] = "fp"  # 假正例

        else:

            predicted_labels.append(0)  # 预测为正常

            if anomaly_label == 1:

                false_negatives.append([timestamp, game_id, operating_system, row.value, anomaly_label])

                row_info["type"] = "fn"  # 假反例

            else:

                true_negatives.append([timestamp, game_id, operating_system, row.value, anomaly_label])



        # 构建检测结果数据框

        if row_info["type"] != "":

            if detections_dataframe is None:

                detections_dataframe = pd.DataFrame(row_info, index=[0])

            else:

                detections_dataframe = pd.concat([detections_dataframe, pd.DataFrame(row_info, index=[0])], ignore_index=True)



    '''

    # 获取当前时间并格式化

    current_time = datetime.now()

    formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S")



    # 写入检测结果到CSV文件

    # detections_dataframe.to_csv(f"./log/detects_result_{formatted_time}.csv", index=False)

    '''

    

    # 输出模型评估指标

    print(f'accuracy: {metrics.accuracy_score(actual_labels, predicted_labels)*100:.2f}%')  # 准确度

    print(f'precision: {metrics.precision_score(actual_labels, predicted_labels)*100:.2f}%')  # 精确度

    print(f'recall: {metrics.recall_score(actual_labels, predicted_labels)*100:.2f}%')  # 召回率

    print(f'f1: {metrics.f1_score(actual_labels, predicted_labels):.2f}')  # F1分数

    print("\n检测到的异常点（真阳性+假阳性）:")

    for tp_fp in true_positives + false_negatives:

        print(f"日期: {tp_fp[0]}, 游戏ID: {tp_fp[1]}, 系统: {tp_fp[2]}, 值: {tp_fp[3]}, 标签: {'异常' if tp_fp[4] == 1 else '正常'}")



    # 打印实际异常数据对应的日期和值（从true_positive中获取，因为这些是实际异常且被正确预测）

    print("\n实际异常数据及其详情:")

    for tp in true_positives:

        print(f"日期: {tp[0]}, 游戏ID: {tp[1]}, 系统: {tp[2]}, 值: {tp[3]}, 标签: 异常")



    all_anomalies = true_positives + false_negatives



    # 提取涉及的唯一gid和os

    gids_of_interest = set(item[1] for item in all_anomalies)

    os_of_interest = set(item[2] for item in all_anomalies)



    # 假设我们要为每个gid和os组合分别绘制图表

    for gid in gids_of_interest:

        for os in os_of_interest:

            # 筛选特定gid和os的所有数据

            specific_df = df_data[(df_data['gid'] == gid) & (df_data['os'] == os)]

            

            # 确保数据按时间排序

            specific_df.sort_values('time', inplace=True)

            

            # 绘制数据

            plt.figure(figsize=(10, 6))

            plt.plot(specific_df['time'], specific_df['value'], label=f'Value Over Time for GID: {gid}, OS: {os}', color='blue')

            

            # 标注异常点

            for tp_fn in all_anomalies:

                if tp_fn[1] == gid and tp_fn[2] == os:

                    anomaly_color = 'red' if tp_fn in true_positives else 'pink'

                    plt.scatter(tp_fn[0], tp_fn[3], color=anomaly_color, 

                                 zorder=5)

            

            plt.title(f'Data and Anomalies for Game ID: {gid}, OS: {os}')

            plt.xlabel('Time')

            plt.ylabel('Value')

            plt.legend()

            plt.grid(True)

            plt.tight_layout()

            plt.show()





# 当脚本被直接运行时调用主函数

if __name__ == '__main__':

    main()